use crate::fixtures::{Assertions, Fixture, PluginTestSpec};
use anyhow::{Context, Result};
use camino::Utf8Path;
use itertools::Itertools;
use serde_json::{Map, Value};
use std::fmt::Write as _;
use std::fs;
use std::io::Write;

pub fn generate(fixtures: &[Fixture], output_root: &Utf8Path) -> Result<()> {
    let rust_root = output_root.join("rust");
    let tests_dir = rust_root.join("tests");

    fs::create_dir_all(&tests_dir).context("Failed to create Rust tests directory")?;

    clean_rs_files(&tests_dir)?;

    let doc_fixtures: Vec<_> = fixtures.iter().filter(|f| f.is_document_extraction()).collect();
    let api_fixtures: Vec<_> = fixtures.iter().filter(|f| f.is_plugin_api()).collect();

    let mut grouped = doc_fixtures
        .into_iter()
        .into_group_map_by(|fixture| fixture.category().to_string())
        .into_iter()
        .collect::<Vec<_>>();
    grouped.sort_by(|a, b| a.0.cmp(&b.0));

    for (category, mut fixtures) in grouped {
        fixtures.sort_by(|a, b| a.id.cmp(&b.id));
        let file_name = format!("{}_test.rs", sanitize_identifier(&category));
        let content = render_category(&category, &fixtures)?;
        let path = tests_dir.join(file_name);
        fs::write(&path, content).with_context(|| format!("Writing {}", path))?;
    }

    if !api_fixtures.is_empty() {
        generate_plugin_api_tests(&api_fixtures, &tests_dir)?;
    }

    Ok(())
}

fn clean_rs_files(dir: &Utf8Path) -> Result<()> {
    if !dir.exists() {
        return Ok(());
    }

    for entry in fs::read_dir(dir.as_std_path())? {
        let entry = entry?;
        if entry.path().extension().is_some_and(|ext| ext == "rs") {
            fs::remove_file(entry.path())?;
        }
    }

    Ok(())
}

fn render_category(category: &str, fixtures: &[&Fixture]) -> Result<String> {
    let mut buffer = Vec::new();
    writeln!(
        buffer,
        "// Auto-generated tests for {category} fixtures.\n#![allow(clippy::too_many_lines)]"
    )?;
    writeln!(buffer, "use e2e_rust::{{assertions, resolve_document}};")?;
    writeln!(buffer, "use kreuzberg::core::config::ExtractionConfig;")?;

    let needs_error_import = fixtures.iter().any(|fixture| {
        !fixture.skip().requires_feature.is_empty() || !fixture.document().requires_external_tool.is_empty()
    });

    if needs_error_import {
        writeln!(buffer, "use kreuzberg::KreuzbergError;\n")?;
    } else {
        writeln!(buffer)?;
    }

    for fixture in fixtures {
        buffer.write_all(render_test(fixture)?.as_bytes())?;
    }

    Ok(String::from_utf8(buffer)?)
}

fn render_test(fixture: &Fixture) -> Result<String> {
    let mut test_body = String::new();

    let test_name = format!("test_{}", sanitize_identifier(&fixture.id));
    writeln!(
        test_body,
        "#[test]\nfn {test_name}() {{\n    // {}\n",
        fixture.description
    )?;

    let doc_path = &fixture.document().path;
    writeln!(
        test_body,
        "    let document_path = resolve_document(\"{}\");",
        escape_rust_string(doc_path)
    )?;

    if fixture.skip().if_document_missing {
        writeln!(
            test_body,
            "    if !document_path.exists() {{\n        println!(\"Skipping {id}: missing document at {{}}\", document_path.display());\n        return;\n    }}",
            id = fixture.id
        )?;
    }

    let config_literal = render_config_literal(&fixture.extraction().config)?;
    if config_literal.trim().is_empty() || config_literal.trim() == "{}" {
        writeln!(test_body, "    let config = ExtractionConfig::default();\n")?;
    } else {
        writeln!(
            test_body,
            "    let config: ExtractionConfig = serde_json::from_str(r#\"{config}\"#)\n        .expect(\"Fixture config should deserialize\");\n",
            config = config_literal
        )?;
    }

    writeln!(
        test_body,
        "    let result = match kreuzberg::extract_file_sync(&document_path, None, &config) {{"
    )?;
    if !fixture.skip().requires_feature.is_empty() || !fixture.document().requires_external_tool.is_empty() {
        writeln!(
            test_body,
            "        Err(KreuzbergError::MissingDependency(dep)) => {{\n            println!(\"Skipping {id}: missing dependency {{dep}}\", dep=dep);\n            return;\n        }},",
            id = fixture.id
        )?;
        writeln!(
            test_body,
            "        Err(KreuzbergError::UnsupportedFormat(fmt)) => {{\n            println!(\"Skipping {id}: unsupported format {{fmt}} (requires optional tool)\", fmt=fmt);\n            return;\n        }},",
            id = fixture.id
        )?;
    }
    writeln!(
        test_body,
        "        Err(err) => panic!(\"Extraction failed for {id}: {{err:?}}\"),\n        Ok(result) => result,\n    }};\n",
        id = fixture.id
    )?;

    test_body.push_str(&render_assertions(&fixture.assertions()));

    writeln!(test_body, "}}\n")?;

    Ok(test_body)
}

fn render_config_literal(config: &Map<String, Value>) -> Result<String> {
    if config.is_empty() {
        Ok(String::new())
    } else {
        let value = Value::Object(config.clone());
        Ok(serde_json::to_string_pretty(&value)?)
    }
}

fn render_assertions(assertions: &Assertions) -> String {
    let mut buffer = String::new();

    if !assertions.expected_mime.is_empty() {
        buffer.push_str(&format!(
            "    assertions::assert_expected_mime(&result, &{});\n",
            render_string_slice(&assertions.expected_mime)
        ));
    }

    if let Some(min) = assertions.min_content_length {
        buffer.push_str(&format!("    assertions::assert_min_content_length(&result, {min});\n"));
    }

    if let Some(max) = assertions.max_content_length {
        buffer.push_str(&format!("    assertions::assert_max_content_length(&result, {max});\n"));
    }

    if !assertions.content_contains_any.is_empty() {
        buffer.push_str(&format!(
            "    assertions::assert_content_contains_any(&result, &{});\n",
            render_string_slice(&assertions.content_contains_any)
        ));
    }

    if !assertions.content_contains_all.is_empty() {
        buffer.push_str(&format!(
            "    assertions::assert_content_contains_all(&result, &{});\n",
            render_string_slice(&assertions.content_contains_all)
        ));
    }

    if let Some(tables) = assertions.tables.as_ref() {
        let min = tables
            .min
            .map(|value| format!("Some({value})"))
            .unwrap_or_else(|| "None".into());
        let max = tables
            .max
            .map(|value| format!("Some({value})"))
            .unwrap_or_else(|| "None".into());
        buffer.push_str(&format!("    assertions::assert_table_count(&result, {min}, {max});\n",));
    }

    if let Some(languages) = assertions.detected_languages.as_ref() {
        let expected = render_string_slice(&languages.expects);
        let min_conf = languages
            .min_confidence
            .map(|v| format!("Some({v})"))
            .unwrap_or_else(|| "None".into());
        buffer.push_str(&format!(
            "    assertions::assert_detected_languages(&result, &{expected}, {min_conf});\n"
        ));
    }

    if !assertions.metadata.is_empty() {
        for (path, expectation) in &assertions.metadata {
            buffer.push_str(&format!(
                "    assertions::assert_metadata_expectation(&result, \"{}\", &{});\n",
                escape_rust_string(path),
                render_json_expression(expectation)
            ));
        }
    }

    buffer
}

fn render_json_expression(value: &serde_json::Value) -> String {
    format!("serde_json::json!({})", value)
}

fn render_string_slice(values: &[String]) -> String {
    let parts = values
        .iter()
        .map(|value| format!("\"{}\"", escape_rust_string(value)))
        .collect::<Vec<_>>()
        .join(", ");
    format!("[{}]", parts)
}

fn sanitize_identifier(input: &str) -> String {
    let mut ident = input
        .chars()
        .map(|c| match c {
            'a'..='z' | 'A'..='Z' | '0'..='9' => c.to_ascii_lowercase(),
            _ => '_',
        })
        .collect::<String>();
    while ident.contains("__") {
        ident = ident.replace("__", "_");
    }
    ident.trim_matches('_').to_string()
}

fn escape_rust_string(value: &str) -> String {
    value
        .replace('\\', "\\\\")
        .replace('"', "\\\"")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
        .replace('\t', "\\t")
}

fn generate_plugin_api_tests(fixtures: &[&Fixture], output_dir: &Utf8Path) -> Result<()> {
    let test_file = output_dir.join("plugin_apis_test.rs");

    let mut buffer = String::new();

    writeln!(buffer, "// Auto-generated tests for plugin API fixtures.")?;
    writeln!(buffer, "#![allow(clippy::too_many_lines)]")?;
    writeln!(buffer)?;

    writeln!(buffer, "use kreuzberg::core::config::ExtractionConfig;")?;
    writeln!(buffer, "use kreuzberg::plugins::{{list_validators, clear_validators}};")?;
    writeln!(
        buffer,
        "use kreuzberg::plugins::{{list_post_processors, clear_extractors, list_extractors}};"
    )?;
    writeln!(
        buffer,
        "use kreuzberg::plugins::{{list_ocr_backends, clear_ocr_backends, unregister_ocr_backend}};"
    )?;
    writeln!(buffer, "use kreuzberg::plugins::{{unregister_extractor}};")?;
    writeln!(
        buffer,
        "use kreuzberg::{{detect_mime_type, detect_mime_type_from_bytes, get_extensions_for_mime}};"
    )?;
    writeln!(buffer)?;

    for fixture in fixtures {
        generate_plugin_test(fixture, &mut buffer)?;
    }

    fs::write(&test_file, buffer)
        .with_context(|| format!("Failed to write Rust plugin API test file {}", test_file))?;

    Ok(())
}

fn generate_plugin_test(fixture: &Fixture, buf: &mut String) -> Result<()> {
    let test_spec = fixture
        .test_spec
        .as_ref()
        .with_context(|| format!("Fixture {} missing test_spec", fixture.id))?;

    let test_name = format!("test_{}", sanitize_identifier(&fixture.id));

    writeln!(buf, "#[test]")?;
    writeln!(buf, "fn {}() {{", test_name)?;
    writeln!(buf, "    // {}", fixture.description)?;
    writeln!(buf)?;

    match test_spec.pattern.as_str() {
        "simple_list" => generate_simple_list_test_rust(test_spec, buf)?,
        "clear_registry" => generate_clear_registry_test_rust(test_spec, buf)?,
        "graceful_unregister" => generate_graceful_unregister_test_rust(test_spec, buf)?,
        "config_from_file" => generate_config_from_file_test_rust(test_spec, buf)?,
        "config_discover" => generate_config_discover_test_rust(test_spec, buf)?,
        "mime_from_bytes" => generate_mime_from_bytes_test_rust(test_spec, buf)?,
        "mime_from_path" => generate_mime_from_path_test_rust(test_spec, buf)?,
        "mime_extension_lookup" => generate_mime_extension_lookup_test_rust(test_spec, buf)?,
        _ => anyhow::bail!("Unknown test pattern: {}", test_spec.pattern),
    }

    writeln!(buf, "}}")?;
    writeln!(buf)?;

    Ok(())
}

fn generate_simple_list_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let func_name = &test_spec.function_call.name;
    let assertions = &test_spec.assertions;

    let rust_func_name = match func_name.as_str() {
        "list_document_extractors" => "list_extractors",
        _ => func_name.as_str(),
    };

    writeln!(
        buf,
        "    let result = {}().expect(\"Failed to list registry\");",
        rust_func_name
    )?;

    if let Some(item_type) = &assertions.list_item_type
        && item_type == "string"
    {
        writeln!(buf, "    assert!(result.iter().all(|s| !s.is_empty()));")?;
    }

    if let Some(contains) = &assertions.list_contains {
        writeln!(
            buf,
            "    assert!(result.contains(&\"{}\".to_string()));",
            escape_rust_string(contains)
        )?;
    }

    Ok(())
}

fn generate_clear_registry_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let func_name = &test_spec.function_call.name;

    if func_name == "clear_post_processors" {
        writeln!(buf, "    // Clear post-processors via registry (no helper function)")?;
        writeln!(
            buf,
            "    let registry = kreuzberg::plugins::registry::get_post_processor_registry();"
        )?;
        writeln!(
            buf,
            "    let mut registry = registry.write().expect(\"Failed to acquire write lock\");"
        )?;
        writeln!(buf, "    registry.shutdown_all().expect(\"Failed to clear registry\");")?;
        writeln!(buf, "    drop(registry);")?;
        writeln!(buf)?;
        writeln!(
            buf,
            "    let result = list_post_processors().expect(\"Failed to list registry\");"
        )?;
        writeln!(buf, "    assert!(result.is_empty());")?;
    } else {
        let rust_func_name = match func_name.as_str() {
            "clear_document_extractors" => "clear_extractors",
            _ => func_name.as_str(),
        };

        writeln!(buf, "    {}().expect(\"Failed to clear registry\");", rust_func_name)?;

        let list_func = rust_func_name.replace("clear_", "list_");
        writeln!(
            buf,
            "    let result = {}().expect(\"Failed to list registry\");",
            list_func
        )?;
        writeln!(buf, "    assert!(result.is_empty());")?;
    }

    Ok(())
}

fn generate_graceful_unregister_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let func_name = &test_spec.function_call.name;
    let arg = &test_spec.function_call.args[0];
    let arg_str = arg
        .as_str()
        .with_context(|| format!("Expected string argument in {}", func_name))?;

    let rust_func_name = match func_name.as_str() {
        "unregister_document_extractor" => "unregister_extractor",
        _ => func_name.as_str(),
    };

    writeln!(
        buf,
        "    {}(\"{}\").expect(\"Unregister should not fail\");",
        rust_func_name,
        escape_rust_string(arg_str)
    )?;

    Ok(())
}

fn generate_config_from_file_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let setup = test_spec.setup.as_ref().context("config_from_file requires setup")?;
    let file_content = setup
        .temp_file_content
        .as_ref()
        .context("config_from_file requires temp_file_content")?;
    let file_name = setup
        .temp_file_name
        .as_ref()
        .context("config_from_file requires temp_file_name")?;

    writeln!(
        buf,
        "    let temp_dir = tempfile::tempdir().expect(\"Failed to create temp dir\");"
    )?;
    writeln!(
        buf,
        "    let config_path = temp_dir.path().join(\"{}\");",
        escape_rust_string(file_name)
    )?;
    let toml_content = file_content.replace("\\n", "\n");
    writeln!(
        buf,
        "    std::fs::write(&config_path, r#\"{}\"#).expect(\"Failed to write config file\");",
        toml_content
    )?;
    writeln!(buf)?;

    writeln!(buf, "    let config = ExtractionConfig::from_file(&config_path)")?;
    writeln!(buf, "        .expect(\"Failed to load config from file\");")?;
    writeln!(buf)?;

    generate_object_property_assertions_rust(&test_spec.assertions, buf)?;

    Ok(())
}

fn generate_config_discover_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let setup = test_spec.setup.as_ref().context("config_discover requires setup")?;
    let file_content = setup
        .temp_file_content
        .as_ref()
        .context("config_discover requires temp_file_content")?;
    let file_name = setup
        .temp_file_name
        .as_ref()
        .context("config_discover requires temp_file_name")?;
    let subdir = setup
        .subdirectory_name
        .as_ref()
        .context("config_discover requires subdirectory_name")?;

    writeln!(
        buf,
        "    let temp_dir = tempfile::tempdir().expect(\"Failed to create temp dir\");"
    )?;
    writeln!(
        buf,
        "    let config_path = temp_dir.path().join(\"{}\");",
        escape_rust_string(file_name)
    )?;
    let toml_content = file_content.replace("\\n", "\n");
    writeln!(
        buf,
        "    std::fs::write(&config_path, r#\"{}\"#).expect(\"Failed to write config file\");",
        toml_content
    )?;
    writeln!(buf)?;

    writeln!(
        buf,
        "    let subdir = temp_dir.path().join(\"{}\");",
        escape_rust_string(subdir)
    )?;
    writeln!(
        buf,
        "    std::fs::create_dir(&subdir).expect(\"Failed to create subdirectory\");"
    )?;
    writeln!(buf)?;

    writeln!(
        buf,
        "    let original_dir = std::env::current_dir().expect(\"Failed to get current dir\");"
    )?;
    writeln!(
        buf,
        "    std::env::set_current_dir(&subdir).expect(\"Failed to change directory\");"
    )?;
    writeln!(buf)?;
    writeln!(buf, "    let config = ExtractionConfig::discover()")?;
    writeln!(buf, "        .expect(\"Failed to discover config\");")?;
    writeln!(buf, "    assert!(config.is_some());")?;
    writeln!(buf, "    let config = config.unwrap();")?;
    writeln!(buf)?;
    writeln!(buf, "    // Restore original directory")?;
    writeln!(
        buf,
        "    std::env::set_current_dir(&original_dir).expect(\"Failed to restore directory\");"
    )?;
    writeln!(buf)?;

    generate_object_property_assertions_rust(&test_spec.assertions, buf)?;

    Ok(())
}

fn generate_mime_from_bytes_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let setup = test_spec.setup.as_ref().context("mime_from_bytes requires setup")?;
    let test_data = setup.test_data.as_ref().context("mime_from_bytes requires test_data")?;
    let assertions = &test_spec.assertions;

    writeln!(buf, "    let data = b\"{}\";", test_data)?;

    writeln!(buf, "    let result = detect_mime_type_from_bytes(data)")?;
    writeln!(buf, "        .expect(\"Failed to detect MIME type from bytes\");")?;

    if let Some(expected) = &assertions.string_contains {
        writeln!(
            buf,
            "    assert!(result.contains(\"{}\"));",
            escape_rust_string(expected)
        )?;
    }

    Ok(())
}

fn generate_mime_from_path_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let setup = test_spec.setup.as_ref().context("mime_from_path requires setup")?;
    let file_name = setup
        .temp_file_name
        .as_ref()
        .context("mime_from_path requires temp_file_name")?;
    let file_content = setup
        .temp_file_content
        .as_ref()
        .context("mime_from_path requires temp_file_content")?;
    let assertions = &test_spec.assertions;

    writeln!(
        buf,
        "    let temp_dir = tempfile::tempdir().expect(\"Failed to create temp dir\");"
    )?;
    writeln!(
        buf,
        "    let file_path = temp_dir.path().join(\"{}\");",
        escape_rust_string(file_name)
    )?;
    writeln!(
        buf,
        "    std::fs::write(&file_path, \"{}\").expect(\"Failed to write file\");",
        escape_rust_string(file_content)
    )?;
    writeln!(buf)?;

    writeln!(buf, "    let result = detect_mime_type(&file_path, true)")?;
    writeln!(buf, "        .expect(\"Failed to detect MIME type\");")?;

    if let Some(expected) = &assertions.string_contains {
        writeln!(
            buf,
            "    assert!(result.contains(\"{}\"));",
            escape_rust_string(expected)
        )?;
    }

    Ok(())
}

fn generate_mime_extension_lookup_test_rust(test_spec: &PluginTestSpec, buf: &mut String) -> Result<()> {
    let func_call = &test_spec.function_call;
    let arg = &func_call.args[0];
    let mime_type = arg.as_str().context("Expected string argument for MIME type")?;
    let assertions = &test_spec.assertions;

    writeln!(
        buf,
        "    let result = get_extensions_for_mime(\"{}\")",
        escape_rust_string(mime_type)
    )?;
    writeln!(buf, "        .expect(\"Failed to get extensions for MIME type\");")?;

    if let Some(contains) = &assertions.list_contains {
        writeln!(
            buf,
            "    assert!(result.contains(&\"{}\".to_string()));",
            escape_rust_string(contains)
        )?;
    }

    Ok(())
}

fn generate_object_property_assertions_rust(
    assertions: &crate::fixtures::PluginAssertions,
    buf: &mut String,
) -> Result<()> {
    if !assertions.object_properties.is_empty() {
        for prop in &assertions.object_properties {
            let path = &prop.path;

            if let Some(exists) = prop.exists {
                if exists {
                    writeln!(buf, "    // Verify {} exists", path)?;

                    if path.contains('.') {
                        let parts: Vec<&str> = path.split('.').collect();
                        if parts.len() == 2 {
                            writeln!(buf, "    assert!(config.{}.is_some());", parts[0])?;
                        }
                    } else {
                        writeln!(buf, "    let _ = &config.{};", path)?;
                    }
                } else {
                    writeln!(buf, "    // Verify {} does not exist (not implemented)", path)?;
                }
            }

            if let Some(value) = &prop.value {
                if path.contains('.') {
                    let parts: Vec<&str> = path.split('.').collect();
                    if parts.len() == 2 {
                        match value {
                            Value::Number(n) => {
                                writeln!(
                                    buf,
                                    "    assert_eq!(config.{}.as_ref().unwrap().{}, {});",
                                    parts[0], parts[1], n
                                )?;
                            }
                            Value::String(s) => {
                                writeln!(
                                    buf,
                                    "    assert_eq!(config.{}.as_ref().unwrap().{}, \"{}\");",
                                    parts[0],
                                    parts[1],
                                    escape_rust_string(s)
                                )?;
                            }
                            Value::Bool(b) => {
                                writeln!(
                                    buf,
                                    "    assert_eq!(config.{}.as_ref().unwrap().{}, {});",
                                    parts[0], parts[1], b
                                )?;
                            }
                            _ => {
                                writeln!(buf, "    // Complex value assertion not yet implemented for {}", path)?;
                            }
                        }
                    }
                } else {
                    match value {
                        Value::Number(n) => {
                            writeln!(buf, "    assert_eq!(config.{}, {});", path, n)?;
                        }
                        Value::String(s) => {
                            writeln!(buf, "    assert_eq!(config.{}, \"{}\");", path, escape_rust_string(s))?;
                        }
                        Value::Bool(b) => {
                            writeln!(buf, "    assert_eq!(config.{}, {});", path, b)?;
                        }
                        _ => {
                            writeln!(buf, "    // Complex value assertion not yet implemented for {}", path)?;
                        }
                    }
                }
            }
        }
    }

    Ok(())
}
